--[[ LICENSE
Copyright (c) 2006-2007, Kyle Smith
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-- ]]

--[[ DOCUMENTATION
Description:

Roster is an Ace3 library for ...

API:

Roster:Foo(bar, baz)
-- Description
--
-- Usage:
--   Roster:Foo(bar, baz)
--]]

-- roster[attributeName][rosterid]
local roster = {
	-- "normal"
	unitid = {},
	name = {},
	level = {},
	class = {},
	fileName = {},

	-- party-related
	partyIndex = {},

	-- party/raid (not pets!)
	isLeader = {},
	rank = {},
	isML = {},
	online = {},

	-- raid-related
	raidIndex = {},
	subgroup = {},
	zone = {},
	role = {},
	isDead = {},

	-- pet-related
	owner = {},
	pet = {},
}

--[[ LIBRARY STARTS HERE ]]---------------------------------------------------

local lib
do
	local prev
	lib = LibStub and LibStub:NewLibrary("Roster-3.0", "$Rev: 1 $")
	if not lib then return end

	if not lib.roster then 
		lib.roster = roster
	else
		roster = lib.roster
	end
	
	local event = assert(LibStub:GetLibrary("AceEvent-3.0"), "Roster-3.0 requires AceEvent-3.0")
	event:Embed(lib)
	
	local bucket = USE_BUCKET and LibStub:GetLibrary("AceBucket-3.0")
	
	if bucket then
		bucket:Embed(lib)
		lib:RegisterBucketEvent(
			{"RAID_ROSTER_UPDATE", "PARTY_MEMBERS_CHANGED"},
			0.2, "UpdateRoster")
		lib:RegisterBucketEvent("UNIT_PET", 0.2, "UpdatePets")
	else
		lib:RegisterEvent("RAID_ROSTER_UPDATE", "UpdateRoster")
		lib:RegisterEvent("PARTY_MEMBERS_CHANGED", "UpdateRoster")
		lib:RegisterEvent("UNIT_PET", "UpdatePets")
	end

	lib:RegisterEvent("PLAYER_ENTERING_WORLD")
	lib:SendMessage("Roster30_Enabled")
end

-- rosterid is UnitName(unitid) for players
--         and UnitName(owner) ..":".. UnitName(unitid) for pets
function lib:GetAttribute (rosterid, attributeName)
	assert(type(rosterid) == "string", "expected string")
	if roster[attributeName] then
		return roster[attributeName][rosterid]
	end
end

function lib:SetAttribute (rosterid, attributeName, value)
	assert(type(unitName) == "string", "expected string")
	if not roster[attributeName] then
		roster[attributeName] = {}
	end

	roster[attributeName][rosterid] = value
end

function lib:GetUnitAttribute (unitid, attributeName)
	assert(type(unitName) == "string", "expected string")
	local rosterid = self:IdentifyUnit(unitid)
	return self:GetAttribute(rosterid, attributeName)
end

function lib:SetUnitAttribute (unitid, attributeName, value)
	assert(type(unitName) == "string", "expected string")
	local rosterid = self:IdentifiyUnit(unitid)
	return self:SetAttribute(rosterid, attributeName, value)
end

function lib:IterateAttribute (attributeName, filter)
	local attr_table = roster[attributeName]
	assert(type(attr_table) == "table", "bad argument #1 to 'IterateAttribute' (\""..attributeName.."\" is not a valid attribute)")
	local id

	-- generate a closure so the iterator will be reentrant
	return function ()
		repeat
			id = next(attr_table, id)
			local value = attr_table[id]

			if not filter or filter(id, value) then
				return id, value
			end
		until id == nil 
	end
end

--[[ UPDATE HOOKS ]]----------------------------------------------------------

local roster_update_hooks = {
--[[
	Each of the following table has two purpose.
	First, it indexes the hook function by hook_id.
	Second, it indexes the hook_id by order.
]]
	playerunit = {},
	playerpet = {},
	partyunit = {},
	partypet = {},
	raidunit = {},
	raidpet = {},
}


function lib:RegisterUpdateHook (hook_type, hook_id, func, order)
	assert(hook_id ~= nil and type(hook_id) ~= "number", "expected non-nil and not a number")
	assert(type(func) == "function", "expected function")

	local hooks = roster_update_hooks[hook_type]
	assert(type(hooks) == "table", "invalid hook_type")

	hooks[hook_id] = func
	if not order then
		table.insert(hooks, hook_id)
	else
		table.insert(hooks, order, hook_id)
	end
end


function lib:UnregisterUpdateHook (hook_type, hook_id)
	assert(hook_id ~= nil and type(hook_id) ~= "number", "expected non-nil and not a number")

	local hooks = roster_update_hooks[hook_type]
	assert(type(hooks) == "table", "invalid hook_type")

	hooks[hook_id] = nil

	for k, v in ipairs(hooks) do
		if v == hook_id then
			table.remove(hooks, k)
			return
		end
	end
end


do
	local old_attrs = setmetatable({}, { __newindex = function () end })
	function lib:CallUpdateHooks (hook_type, rosterid, ...)
		assert(type(hook_type) == "string", "expected string")
		
		local hooks = roster_update_hooks[hook_type]
		assert(type(hooks) == "table", "invalid hook_type")

		-- save
		for attr, attr_table in pairs(roster) do
			rawset(old_attrs, attr, attr_table[rosterid])
		end
		
		-- update
		for _, hook_id in ipairs(hooks) do
			hooks[hook_id](hook_type, rosterid, ...)
		end

		-- compare
		for attr, attr_table in pairs(roster) do
			local value = attr_table[rosterid]
			if value == old_attrs[attr] then
				rawset(old_attrs, attr, nil)
			end
		end

		-- trigger
		if next(old_attrs) ~= nil then
			self:SendMessage("Roster30_UnitChanged", rosterid, old_attrs)
		end

		-- clean up
		for attr in pairs(old_attrs) do
			rawset(old_attrs, attr, nil)
		end
	end
end

--[[ PARTY TRANSITIONS ]]-----------------------------------------------------

-- I stole this from Grid
do
	local prevInArena = false
	local prevInBG = false
	local prevInRaid = false
	local prevInParty = false
	function lib:PLAYER_ENTERING_WORLD()
		-- this is needed to trigger an update when switching from one
		-- BG directly to another
		prevInArena = false
		prevInBG = false
		
		return self:PartyTransitionCheck()
	end

	function lib:PartyTransitionCheck()
		local inParty = (GetNumPartyMembers() > 0 or
				 GetNumRaidMembers() > 0)
		local inRaid = GetNumRaidMembers() > 0
		local inBG = select(2, IsInInstance()) == "pvp"
		local inArena = select(2, IsInInstance()) == "arena"

		-- arena -> arena, bg, raid, party, solo
		-- bg    -> arena, bg, raid, party, solo
		-- raid  -> arena, bg, party, solo
		-- party -> arena, bt, raid, solo
		
		if inArena then
			if not prevInArena then
				self:SendMessage("Roster30_JoinedArena")
			end
			inParty = false
			inRaid = false
			inBG = false
		elseif inBG then
			if not prevInBG then
				self:SendMessage("Roster30_JoinedBattleground")
			end
			inParty = false
			inRaid = false
			inArena = false
		elseif inRaid then
			if not prevInRaid or prevInBG then
				self:SendMessage("Roster30_JoinedRaid")
			end
			inParty = false
			inBG = false
			inArena = false
		elseif inParty then
			if not prevInParty or prevInRaid or prevInBG or prevInArena then
				self:SendMessage("Roster30_JoinedParty")
			end
			inRaid = false
			inBG = false
			inArena = false
		else
			self:SendMessage("Roster30_LeftParty")
		end

		prevInRaid = inRaid
		prevInParty = inParty
		prevInBG = inBG
		prevInArena = inArena
	end
end

--[[ UNIT STUFF ]]------------------------------------------------------------

local party_units = {}
local raid_units = {}
local pet_of_unit = {}
local owner_of_unit = {}

do
	-- populate unit tables
	local function register_unit (tbl, unit, pet)
		table.insert(tbl, unit)
		pet_of_unit[unit] = pet
		owner_of_unit[pet] = unit
	end
	
	register_unit(party_units, "player", "pet")
	
	for i = 1, MAX_PARTY_MEMBERS do
		register_unit(party_units, ("party%d"):format(i),
			      ("partypet%d"):format(i))
	end
	for i = 1, MAX_RAID_MEMBERS do
		register_unit(raid_units, ("raid%d"):format(i),
			      ("raidpet%d"):format(i))
	end
end


-- Unit Name -> Roster ID
function lib:IdentifyUnitByName (unitName)
	assert(type(unitName) == "string", "expected string")

	-- check for a player
	if roster.name[unitName] == unitName then
		return unitName
	end

	-- check for a pet
	for rosterid, name in self:IterateAttribute("name") do
		if name == unitName then
			return rosterid
		end
	end
end

-- Unit ID -> Roster ID
function lib:IdentifyUnitByUnitid (unitid)
	assert(type(unitid) == "string", "bag argument #1 to 'IdentifyUnitByUnitid' (expected string, got "..type(unitid)..")")

	-- let's not waste our time looking for things that aren't there
	if not UnitExists(unitid) then
		return
	end

	-- is it a member of our raid or party?
	--
	-- note that this doesn't match raid/party pets
	if raid_units[unitid] or party_units[unitid] or
		UnitInRaid(unitid) or UnitInParty(unitid) then

		return UnitName(unitid)
	end

	-- is it a pet?
	local owner = owner_of_unit[unitid]
	if owner then
		return ("%s:%s"):format(UnitName(owner), UnitName(unitid))
	end

	-- if it's not a primary unit then start looking for pets
	if UnitPlayerOrPetInRaid(unitid) or UnitPlayerOrPetInParty(unitid) then
		for pet in pairs(owner_of_unit) do
			if UnitExists(pet) and UnitIsUnit(unitid, pet) then
				return self:IdentifyUnitByUnitid(pet)
			end
		end
	end
end

function lib:IdentifyUnit (name_or_id)
	assert(type(name_or_id) == "string", "expected string")

	if UnitExists(name_or_id) then
		return self:IdentifyUnitByUnitid(name_or_id)
	else
		return self:IdentifyUnitByName(name_or_id)
	end
end

do
	local units_to_remove = {}
	local units_added = {}
	local unknown_units = {}

	function lib:UpdateUnit (hook_type, unitid, ...)
		local name = UnitName(unitid)

		if name ~= UNKNOWNOBJECT and name ~= UNKNOWNBEING and
			not UnitIsCharmed(unitid) then

			local rosterid = self:IdentifyUnitByUnitid(unitid)
			
			self:CallUpdateHooks(hook_type, unitid, ...)

			return rosterid
		else
			-- mark unit as unknown
			unknown_units[unitid] = hook_type
		end

		return nil
	end

	function lib:UpdatePlayerUnit (unitid)
		return self:UpdateUnit("playerunit", unitid)
	end

	function lib:UpdatePlayerPet (unitid, owner)
		return self:UpdateUnit("playerpet", unitid, owner)
	end

	function lib:UpdatePartyUnit (unitid, partyIndex)
		return self:UpdateUnit("partyunit", rosterid, unitid,
				       partyIndex)
	end

	function lib:UpdatePartyPet (unitid, owner)
		return self:UpdateUnit("partypet", rosterid, unitid)
	end

	function lib:UpdateRaidUnit (unitid, raidIndex)
		return self:UpdateUnit("raidunit", rosterid, unitid)
	end

	function lib:UpdateRaidPet (unitid, owner)
		self:CallUpdateHooks("raidpet", rosterid, unitid)
	end

	local function UnitSeen (unitid, rosterid)
		if rosterid and units_to_remove[rosterid] then
			table.insert(units_added, rosterid)
			units_to_remove[rosterid] = nil
			unknown_units[unitid] = nil
		end
	end
	
	function lib:Debug(msg)
		ChatFrame3:AddMessage(msg)
	end

	function lib:UpdateRoster ()
		self:Debug("UpdateRoster()")

		for id, unit in pairs(roster.unitid) do
			units_to_remove[id] = true
		end

		for raidIndex = 1, GetNumRaidMembers() do
			local unit = raid_units[raidIndex]
			local unitpet = pet_of_unit[unit]

			if unit and UnitExists(unit) then
				local id = self:UpdateRaidUnit(unit)
				UnitSeen(unit, id)

				if unitpet and UnitExists(unitpet) then
					id = self:UpdateRaidPet(unitpet)
					UnitSeen(unitpet, id)
				end
			end
		end

		if GetNumRaidMembers() == 0 then
			local unit = "player"
			local unitpet = pet_of_unit[unit]

			local id = self:UpdatePlayerUnit(unit)
			UnitSeen(unit, id)

			if UnitExists(unitpet) then
				id = self:UpdatePlayerPet(unitpet)
				UnitSeen(unitpet, id)
			end

			for i = 1, GetNumPartyMembers() do
				unit = party_units[i - 1]
				unitpet = pet_of_unit[unit]

				if unit and UnitExists(unit) then
					local id = self:UpdatePartyUnit(unit,
									i - 1)
					UnitSeen(unit, id)

					if unitpet and UnitExists(unitpet) then
						id = self:UpdatePartyPet(unitpet)
						UnitSeen(unitpet, id)
					end
				end
			end
		end

		for id in pairs(units_to_remove) do
			self:Debug("Removed", id)

			self:SendMessage("Roster30_UnitLeft", id)
			for attr, attr_tbl in pairs(roster) do
				attr_tbl[id] = nil
			end

			units_to_remove[id] = nil
		end

		self:PartyTransitionCheck()

		for k, id in ipairs(units_added) do
			self:Debug("Added", id)

			self:SendMessage("Roster30_UnitJoined", id)

			units_added[k] = nil
		end
	end

	function lib:UpdatePets (owners)
		for owner in pairs(owners) do
			local ownerid = self:IdentifyUnitByUnitid(owner)
			local old_petid = roster.pet[ownerid]
			local unitpet = pet_of_unit[owner]

			if old_petid then
				units_to_remove[old_petid] = true
			end

			if unitpet and UnitExists(unitpet) then
				local petid = self:UpdatePartyPet(unitpet)
				UnitSeen(petid)
			end
		end

		for id in pairs(units_to_remove) do
			self:Debug("Removed", id)

			self:SendMessage("Roster30_UnitLeft", id)
			for attr, attr_tbl in pairs(roster) do
				attr_tbl[id] = nil
			end

			units_to_remove[id] = nil
		end

		for k, id in ipairs(units_added) do
			self:Debug("Added", id)

			self:SendMessage("Roster30_UnitJoined", id)

			units_added[k] = nil
		end
	end
end


--[[ UPDATE FUNCTIONS ]]------------------------------------------------------

local function UpdateUnitid (hook_type, rosterid, unitid)
	roster.unitid[rosterid] = unitid
end
lib:RegisterUpdateHook("playerunit", "UpdateUnitid", UpdateUnitid)
lib:RegisterUpdateHook("playerpet", "UpdateUnitid", UpdateUnitid)
lib:RegisterUpdateHook("partyunit", "UpdateUnitid", UpdateUnitid)
lib:RegisterUpdateHook("partypet", "UpdateUnitid", UpdateUnitid)
lib:RegisterUpdateHook("raidunit", "UpdateUnitid", UpdateUnitid)
lib:RegisterUpdateHook("raidpet", "UpdateUnitid", UpdateUnitid)


local function UpdateUnitPet (hook_type, rosterid, unitid)
	roster.pet[rosterid] = lib:IdentifyUnitByUnitid(pet_of_unit[unitid])
end
lib:RegisterUpdateHook("playerunit", "UpdateUnitPet", UpdateUnitPet)
lib:RegisterUpdateHook("partyunit", "UpdateUnitPet", UpdateUnitPet)
lib:RegisterUpdateHook("raidunit", "UpdateUnitPet", UpdateUnitPet)


local function UpdatePet (hook_type, rosterid, unitid)
	roster.owner[rosterid] = lib:IdentifyUnitByUnitid(owner_of_unit[unitid])

	roster.name[rosterid] = UnitName(unitid)
	roster.level[rosterid] = UnitLevel(unitid)

	local class, fileName = UnitClass(unitid)
	roster.class[rosterid] = class
	roster.fileName[rosterid] = fileName
	roster.online[rosterid] = UnitIsConnected(unitid)
end
lib:RegisterUpdateHook("playerpet", "UpdatePet", UpdatePet)
lib:RegisterUpdateHook("partypet", "UpdatePet", UpdatePet)
lib:RegisterUpdateHook("raidpet", "UpdatePet", UpdatePet)


local function UpdatePartyUnit (hook_type, rosterid, unitid, partyIndex)
	roster.name[rosterid] = UnitName(unitid)
	roster.level[rosterid] = UnitLevel(unitid)

	local class, fileName = UnitClass(unitid)
	roster.class[rosterid] = class
	roster.fileName[rosterid] = fileName

	roster.isLeader[rosterid] = UnitIsPartyLeader(unitid)

	local lootmethod, masterlooterID = GetLootMethod()
	roster.isML = masterlooterID == partyIndex
	roster.online[rosterid] = UnitIsConnected(unitid)

	-- clear raid-only attributes
	roster.raidIndex[rosterid] = nil
	roster.subgroup[rosterid] = nil
	roster.role[rosterid] = nil
	roster.isDead[rosterid] = nil -- UnitIsDeadOrGhost(unitid)
end
lib:RegisterUpdateHook("playerunit", "UpdatePartyUnit", UpdatePartyUnit)
lib:RegisterUpdateHook("partyunit", "UpdatePartyUnit", UpdatePartyUnit)


local function UpdateRaidUnit (hook_type, rosterid, unitid, raidIndex)
	local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(raidIndex)

	roster.raidIndex[rosterid] = raidIndex
	roster.name[rosterid] = name
	roster.rank[rosterid] = rank
	roster.subgroup[rosterid] = subgroup
	roster.level[rosterid] = level
	roster.class[rosterid] = class
	roster.fileName[rosterid] = fileName
	roster.zone[rosterid] = zone
	roster.online[rosterid] = online
	roster.isDead[rosterid] = isDead
	roster.role[rosterid] = role
	roster.isML[rosterid] = isML

	roster.isLeader[rosterid] = (rank == 2)
end
lib:RegisterUpdateHook("raidunit", "UpdateRaidUnit", UpdateRaidUnit)
